﻿using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.VisualStudio.TestPlatform.UnitTestFramework;
using MugenMvvmToolkit.Infrastructure;
using MugenMvvmToolkit.Interfaces;
using MugenMvvmToolkit.Models;
using MugenMvvmToolkit.Utils;
using MugenMvvmToolkit.ViewModels;
using MugenMvvmToolkitTest.TestInfrastructure;
using Should;

namespace MugenMvvmToolkitTest.ViewModel
{
    public class NotifyProp : NotifyPropertyChangedBase
    {
    }

    public class Observable : IObservable
    {
        #region Fields

        private readonly WeakListenersCollection _listeners;

        #endregion

        #region Constructors

        /// <summary>
        ///     Initializes a new instance of the <see cref="Observable" /> class.
        /// </summary>
        public Observable()
        {
            _listeners = new WeakListenersCollection();
        }

        #endregion

        #region Implementation of IObservable

        public void Publish(object sender, object message)
        {
            _listeners.Publish(sender, message);
        }

        /// <summary>
        ///     Subscribes an instance to events.
        /// </summary>
        /// <param name="instance">The instance to subscribe for event publication.</param>
        public bool Subscribe(object instance)
        {
            return _listeners.Add(instance);            
        }

        /// <summary>
        ///     Unsubscribes the instance from all events.
        /// </summary>
        /// <param name="instance">The instance to unsubscribe.</param>
        public bool Unsubscribe(object instance)
        {
            return _listeners.Remove(instance);
        }

        #endregion
    }

    [TestClass]
    public class RelayCommandTest : TestBase
    {
        #region Fields

        protected static readonly Action<object> NodoAction = o => { };

        #endregion

        #region Test methods

        [TestMethod]
        public void IfExecutionModeNotSetExplicitItShouldBeGettingFromGlobalSettings()
        {
            GlobalSettings.CommandExecutionMode = CommandExecutionType.None;
            var relayCommand = GetCommand(NodoAction);
            relayCommand.ExecutionMode.ShouldEqual(CommandExecutionType.None);

            GlobalSettings.CommandExecutionMode = CommandExecutionType.CanExecuteBeforeExecute;
            relayCommand = GetCommand(NodoAction);
            relayCommand.ExecutionMode.ShouldEqual(CommandExecutionType.CanExecuteBeforeExecute);
        }

        [TestMethod]
        public void IfCanExecuteModeNotSetExplicitItShouldBeGettingFromGlobalSettings()
        {
            GlobalSettings.CommandCanExecuteExecutionMode = ExecutionType.None;
            var relayCommand = GetCommand(NodoAction);
            relayCommand.CanExecuteMode.ShouldEqual(ExecutionType.None);

            GlobalSettings.CommandCanExecuteExecutionMode = ExecutionType.AsynchronousInUi;
            relayCommand = GetCommand(NodoAction);
            relayCommand.CanExecuteMode.ShouldEqual(ExecutionType.AsynchronousInUi);
        }

#if WPF

        [TestMethod]
        public void IfUseCommandManagerNotSetExplicitItShouldBeGettingFromGlobalSettings()
        {
            GlobalSettings.UseCommandManager = true;
            var relayCommand = GetCommand(NodoAction);
            relayCommand.UseCommandManager.ShouldBeTrue();

            GlobalSettings.UseCommandManager = false;
            relayCommand = GetCommand(NodoAction);
            relayCommand.UseCommandManager.ShouldBeFalse();
        }

#endif
        [TestMethod]
        public void CmdShouldNotAddNotifierWhenItCannotExecuteDelegate()
        {
            var notifier = new NotifyProp();
            var relayCommand = GetCommand(NodoAction);
            relayCommand.AddNotifier(notifier).ShouldBeFalse();
            relayCommand.Notifiers.ShouldBeEmpty();
        }

        [TestMethod]
        public void CmdShouldAddNotifierWhenItCanExecuteDelegate()
        {
            var notifier = new NotifyProp();
            var relayCommand = GetCommand(NodoAction, o => true);
            relayCommand.AddNotifier(notifier).ShouldBeTrue();
            relayCommand.Notifiers.Count().ShouldEqual(1);
            relayCommand.Notifiers.Contains(notifier).ShouldBeTrue();
        }

        [TestMethod]
        public void CmdShouldRemoveNotifier()
        {
            var notifier = new NotifyProp();
            var relayCommand = GetCommand(NodoAction, o => true);
            relayCommand.AddNotifier(notifier).ShouldBeTrue();
            relayCommand.Notifiers.Contains(notifier).ShouldBeTrue();

            relayCommand.RemoveNotifier(notifier);
            relayCommand.Notifiers.Contains(notifier).ShouldBeFalse();
        }

        [TestMethod]
        public void CmdShouldClearAllNotifiers()
        {
            var notifier1 = new NotifyProp();
            var notifier2 = new NotifyProp();
            var relayCommand = GetCommand(NodoAction, o => true);
            relayCommand.AddNotifier(notifier1).ShouldBeTrue();
            relayCommand.AddNotifier(notifier2).ShouldBeTrue();
            relayCommand.Notifiers.Contains(notifier1).ShouldBeTrue();
            relayCommand.Notifiers.Contains(notifier2).ShouldBeTrue();

            relayCommand.ClearNotifiers();
            relayCommand.Notifiers.ShouldBeEmpty();
        }

        [TestMethod]
        public void CmdShouldNotAddNotifierIfItIsNotSupported()
        {
            var notifier = new object();
            var relayCommand = GetCommand(NodoAction, o => true);
            relayCommand.AddNotifier(notifier).ShouldBeFalse();
            relayCommand.Notifiers.ShouldBeEmpty();
        }

        [TestMethod]
        public void ExecuteModeShouldChangeBehaviourOfExecuteMethodNone()
        {
            const bool canExecute = false;
            bool isInvoked = false;
            Action<object> action = (o) => isInvoked = true;
            var relayCommand = GetCommand(action, o => canExecute);

            relayCommand.ExecutionMode = CommandExecutionType.None;
            relayCommand.Execute(null);
            isInvoked.ShouldBeTrue();
        }

        [TestMethod]
        public void ExecuteModeShouldChangeBehaviourOfExecuteMethodCallCanExecuteBeforeExecute()
        {
            bool canExecute = false;
            bool isInvoked = false;
            Action<object> action = o => isInvoked = true;
            var relayCommand = GetCommand(action, o => canExecute);

            relayCommand.ExecutionMode = CommandExecutionType.CanExecuteBeforeExecute;
            relayCommand.Execute(null);
            isInvoked.ShouldBeFalse();

            canExecute = true;
            relayCommand.Execute(null);
            isInvoked.ShouldBeTrue();
        }

        [TestMethod]
        public void ExecuteModeShouldChangeBehaviourOfExecuteMethodCallCanExecuteBeforeExecuteAndThrowExceptionIfNeeded()
        {
            const bool canExecute = false;
            Action<object> action = o => { };
            var relayCommand = GetCommand(action, o => canExecute);

            relayCommand.ExecutionMode = CommandExecutionType.CanExecuteBeforeExecuteWithException;
            ShouldThrow<InvalidOperationException>(() => relayCommand.Execute(null));
        }

        [TestMethod]
        public void CanExecuteShouldReturnValueFromDelegate()
        {
            bool canExecute = false;
            var relayCommand = GetCommand(NodoAction, o => canExecute);
            relayCommand.CanExecute(null).ShouldBeFalse();
            canExecute = true;
            relayCommand.CanExecute(null).ShouldBeTrue();
        }

        [TestMethod]
        public void RaiseCanExecuteChangedShouldDependFromCanExecuteModeNone()
        {
            bool isInvoked = false;
            var relayCommand = GetCommand(NodoAction, o => true);
            relayCommand.CanExecuteMode = ExecutionType.None;
            relayCommand.CanExecuteChanged += (sender, args) =>
                                              {
                                                  isInvoked = true;
                                              };
            relayCommand.RaiseCanExecuteChanged();
            isInvoked.ShouldBeTrue();
        }

        [TestMethod]
        public void RaiseCanExecuteChangedShouldDependFromCanExecuteModeInvokeAsynchronous()
        {
            bool isInvoked = false;
            var relayCommand = GetCommand(NodoAction, o => true);
            relayCommand.CanExecuteMode = ExecutionType.AsynchronousInUi;
            relayCommand.CanExecuteChanged += (sender, args) =>
                                              {
                                                  isInvoked = true;
                                              };
            relayCommand.RaiseCanExecuteChanged();
            isInvoked.ShouldBeFalse();
            ThreadManager.InvokeInUiAction();
            isInvoked.ShouldBeTrue();
        }

        [TestMethod]
        public void RaiseCanExecuteChangedShouldDependFromCanExecuteModeInvokeSynchronous()
        {
            bool isInvoked = false;
            var relayCommand = GetCommand(NodoAction, o => true);
            relayCommand.CanExecuteMode = ExecutionType.SynchronousInUi;
            relayCommand.CanExecuteChanged += (sender, args) =>
            {
                isInvoked = true;
            };
            relayCommand.RaiseCanExecuteChanged();
            isInvoked.ShouldBeFalse();
            ThreadManager.InvokeInUiSynchronousAction();
            isInvoked.ShouldBeTrue();
        }

        [TestMethod]
        public void RaiseCanExecuteChangedShouldBeInvokedOnPropertyChangedInListener()
        {
            var notifier = new NotifyProp();
            bool isInvoked = false;
            var relayCommand = GetCommand(NodoAction, o => true);
            relayCommand.CanExecuteMode = ExecutionType.None;
            relayCommand.CanExecuteChanged += (sender, args) =>
            {
                isInvoked = true;
            };
            relayCommand.AddNotifier(notifier);
            notifier.OnPropertyChanged("Test", ExecutionType.None);
            isInvoked.ShouldBeTrue();
        }

        [TestMethod]
        public void RaiseCanExecuteChangedShouldBeInvokedOnStateChangedMessage()
        {
            var notifier = new Observable();
            bool isInvoked = false;
            var relayCommand = GetCommand(NodoAction, o => true);
            relayCommand.CanExecuteMode = ExecutionType.None;
            relayCommand.CanExecuteChanged += (sender, args) =>
            {
                isInvoked = true;
            };
            relayCommand.AddNotifier(notifier);
            notifier.Publish(notifier, new StateChangedMessage());
            isInvoked.ShouldBeTrue();
        }

        [TestMethod]
        public void RaiseCanExecuteChangedShouldSuspendNotification()
        {
            bool isInvoked = false;
            var relayCommand = GetCommand(NodoAction, o => true);
            relayCommand.CanExecuteMode = ExecutionType.None;
            relayCommand.CanExecuteChanged += (sender, args) =>
            {
                isInvoked = true;
            };
            using (relayCommand.SuspendNotifications())
            {
                relayCommand.RaiseCanExecuteChanged();
                using (relayCommand.SuspendNotifications())
                {
                    relayCommand.RaiseCanExecuteChanged();
                }
                isInvoked.ShouldBeFalse();
            }
            isInvoked.ShouldBeTrue();
        }

        [TestMethod]
        public void GetCommandWithNotifiersTest()
        {
            var notifier = new NotifyProp();
            var relayCommand = GetCommand(NodoAction, o => true, notifier);
            relayCommand.Notifiers.ShouldContain(notifier);
        }

        [TestMethod]
        public void GetCommandWithNotifiersAndParametersTest()
        {
            const ExecutionType executionType = ExecutionType.SynchronousInUi;
            var notifier = new NotifyProp();
            var relayCommand = GetCommand(NodoAction, o => true, executionType, ThreadManager, notifier);
            relayCommand.Notifiers.ShouldContain(notifier);
            relayCommand.ThreadManager.ShouldEqual(ThreadManager);
            relayCommand.CanExecuteMode.ShouldEqual(executionType);
        }

        #endregion

        #region Methods

        protected virtual RelayCommand GetCommand(Action<object> execute, ExecutionType? executionMode = null,
            IThreadManager threadManager = null)
        {
            return new RelayCommand(execute, executionMode, threadManager);
        }

        protected virtual RelayCommand GetCommand(Action<object> execute, Predicate<object> canExecute,
            params object[] notifiers)
        {
            return new RelayCommand(execute, canExecute, notifiers);
        }

        protected virtual RelayCommand GetCommand(Action<object> execute, Predicate<object> canExecute,
            ExecutionType? executionMode = null,
            IThreadManager threadManager = null,
            params object[] notifiers)
        {
            return new RelayCommand(execute, canExecute, executionMode, threadManager, notifiers);
        }

        #endregion

    }
}